Skip to content

Conversation

@Lluc24
Copy link
Contributor

@Lluc24 Lluc24 commented Dec 13, 2025

Currently, typeSize reports inconsistent values for standard tuple types (e.g., (A, B)) compared to their semantically equivalent recursive pair encodings (e.g., A *: B *: EmptyTuple).

This discrepancy arises because TupleN is represented as a flat AppliedType, whereas the nested encoding forms a deeper tree structure. As typeSize is often used as a heuristic for complexity or optimization limits, this inconsistency can lead to divergent behavior in the compiler depending on how a tuple is represented syntactically.

This PR modifies TypeSizeAccumulator to canonicalize TupleN types into their recursive *: representation before calculating their size. This ensures that the size metric is consistent regardless of whether the tuple is represented as a flat AppliedType or a nested structural type.

I have added a new unit test in TypesTest that asserts Tuple3[Int, Boolean, Double] and Int *: Boolean *: Double *: EmptyTuple both yield identical typeSize equal to 3.

Thanks to @mbovel for providing the unit test that effectively reproduces this issue and validates the fix.

Fixes #24730

@Lluc24 Lluc24 changed the title Fix inconsistent typeSize for TupleN vs nested pairs Fix inconsistent typeSize calculation for TupleN vs recursive pair encodings Dec 13, 2025
@Lluc24 Lluc24 force-pushed the i24730-TupleN-wrong-typeSize branch from cd3a120 to dca3ea1 Compare December 13, 2025 14:01
@mbovel
Copy link
Member

mbovel commented Dec 14, 2025

We could maybe use normalizedTupleType here:

/** If this is a generic tuple type with arity <= MaxTupleArity, return the
* corresponding TupleN type, otherwise return this.
*/
def normalizedTupleType(using Context): Type =
if self.isGenericTuple then
self.tupleElementTypes match
case Some(elems) if elems.size <= Definitions.MaxTupleArity => defn.tupleType(elems)
case _ => self
else
self

Comment on lines 7137 to 7132
val tpNorm = tp.tryNormalize
if tpNorm.exists then apply(n, tpNorm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val tpNorm = tp.tryNormalize
if tpNorm.exists then apply(n, tpNorm)
val tpNorm = tp.normalized.normalizedTupleType
if tpNorm ne tp then apply(n, tpNorm)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizedTupleType does the opposite of what we want. We need to convert TupleN to *: equivalent. I found this function in TypeUtils.scala that does exactly this:

/** The `*:` equivalent of an instance of a Tuple class */
def toNestedPairs(using Context): Type =
tupleElementTypes match
case Some(types) => TypeOps.nestedPairs(types)
case None => throw new AssertionError("not a tuple")

Also note that tp.normalized on TupleN returns NoType.

Using this function, the code is cleaner and more readable:

def apply(n: Int, tp: Type): Int =
  tp match {
    case tp: AppliedType if defn.isTupleNType(tp) =>
      foldOver(n + 1, tp.toNestedPairs)
    // From here the following code is the same as the original
    case tp: AppliedType =>
      val tpNorm = tp.tryNormalize
      if tpNorm.exists then apply(n, tpNorm)
      else foldOver(n + 1, tp)
    ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to convert TupleN to *: equivalent.

Why not do the opposite? TupleN needs less object allocations.

Also note that tp.normalized on TupleN returns NoType.

Maybe the following would work?

  tp match {
    case tp: AppliedType =>
      val tpNorm = tp.tryNormalize
      if tpNorm.exists then apply(n, tpNorm.normalizedTupleType)
      else foldOver(n + 1, tp.normalizedTupleType)

The above has the advantage that it always normalizes the type, even when it's a tuple.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivation of this issue is that the concatenation type of two tuples is made by a Match Type. When a TupleN and another tuple were to be concatenated, the result type uses the *: equivalent. The resulting typeSize is larger and created a lot of false positives in the algorithm of PR #24661

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but from my understanding, the requirement is simply that both TupleN and nested *: pairs be normalized to the same representation. Does it matter whether TupleN is normalized to nested pairs, or vice versa? If we normalize to TupleN, then both types in the unit tests would have size 1, wouldn't they?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I guess for your current implementation of match types termination checks, it's better if (1, 2) and (1, 2, 3) have sizes 2 and 3.

Previously, `typeSize` reported different values for standard `TupleN`
types (e.g., `(A, B)`) compared to their equivalent recursive pair
encodings (e.g., `A *: B *: EmptyTuple`). This discrepancy occurred
because `TupleN` is a flat `AppliedType`, while the nested encoding
forms a deeper tree structure.

This patch modifies `TypeSizeAccumulator` to canonicalize `TupleN`
types into their recursive `*:` representation before calculating
the size. This ensures that the size metric is consistent regardless
of whether the tuple is represented syntactically or structurally.

This change is verified by a new unit test in `TypesTest`, which
confirms that both `Tuple3[Int, Boolean, Double]` and its recursive
equivalent `Int *: Boolean *: Double *: EmptyTuple` now yield
identical `typeSize` values.

Fixes scala#24730
@Lluc24 Lluc24 force-pushed the i24730-TupleN-wrong-typeSize branch from dca3ea1 to b290509 Compare December 15, 2025 15:14
@mbovel mbovel self-requested a review December 16, 2025 17:24
@mbovel mbovel merged commit 402be90 into scala:main Dec 16, 2025
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Different type sizes for TupleN vs nested pairs encoding

2 participants